hostname in certificate didn't match

描述

某服务商对接公司的在阿里云配置的API网关时,对方开发沟通说我们公司的HTTPS有问题,请求接口后报如下错误:

1
javax.net.ssl.SSLException: hostname in certificate didn't match: <马赛克.com> != <*.alicloudapi.com> OR <*.alicloudapi.com> OR <alicloudapi.com>

看到错误信息,第一反应是HTTPS证书有问题,所以HTTPS握手失败而报错,但是该API网关有多个服务商对结过,均未发生类似的情况。另外,报错信息中的域名alicloudapi.com显然不是我们公司的域名,而是阿里云的域名。

抱着证书有问题的怀疑,在证书检查平台myssl.com做了个证书检测,结果见下图。

image

image

检查报告显示HTTPS的证书一切正常,但是有一点引起了我的注意:检查报告显示该域名有两个证书信息,一个证书是当前域名的,另个域名是似乎是阿里云的域名,该阿里云域名正是报错信息里的域名。

复现

与对方开发进一步沟通后,得知他们使用的JDK版本是1.6版本,于是我也将本地环境切换成JDK1.6后,请求了自己在阿里云配置的多域名证书的HTTPS链接。

1
2
3
4
5
6
7
8
9
10
11
import java.net.URL;
import java.net.URLConnection;

public class Application {
public static void main(String[] args) throws Exception {
URL link = new URL("https://img.wakzz.cn/202004/20200407152546.jpg");
URLConnection connection = link.openConnection();
connection.connect();
System.out.println(connection.getContentLength());
}
}

报错信息如下,果然当使用低版本的JDK1.6请求多证书的HTTPS链接后,HTTPS握手失败,但报错信息与接入商的报错信息并不相同。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
Exception in thread "main" javax.net.ssl.SSLHandshakeException: java.security.cert.CertificateException: No subject alternative DNS name matching img.wakzz.cn found.
at sun.security.ssl.Alerts.getSSLException(Alerts.java:192)
at sun.security.ssl.SSLSocketImpl.fatal(SSLSocketImpl.java:1836)
at sun.security.ssl.Handshaker.fatalSE(Handshaker.java:287)
at sun.security.ssl.Handshaker.fatalSE(Handshaker.java:281)
at sun.security.ssl.ClientHandshaker.serverCertificate(ClientHandshaker.java:1339)
at sun.security.ssl.ClientHandshaker.processMessage(ClientHandshaker.java:203)
at sun.security.ssl.Handshaker.processLoop(Handshaker.java:848)
at sun.security.ssl.Handshaker.process_record(Handshaker.java:784)
at sun.security.ssl.SSLSocketImpl.readRecord(SSLSocketImpl.java:1012)
at sun.security.ssl.SSLSocketImpl.performInitialHandshake(SSLSocketImpl.java:1320)
at sun.security.ssl.SSLSocketImpl.startHandshake(SSLSocketImpl.java:1347)
at sun.security.ssl.SSLSocketImpl.startHandshake(SSLSocketImpl.java:1331)
at sun.net.www.protocol.https.HttpsClient.afterConnect(HttpsClient.java:432)
at sun.net.www.protocol.https.AbstractDelegateHttpsURLConnection.connect(AbstractDelegateHttpsURLConnection.java:185)
at sun.net.www.protocol.https.HttpsURLConnectionImpl.connect(HttpsURLConnectionImpl.java:153)
at Application.main(Application.java:8)
Caused by: java.security.cert.CertificateException: No subject alternative DNS name matching img.wakzz.cn found.
at sun.security.util.HostnameChecker.matchDNS(HostnameChecker.java:208)
at sun.security.util.HostnameChecker.match(HostnameChecker.java:94)
at sun.security.ssl.X509TrustManagerImpl.checkIdentity(X509TrustManagerImpl.java:285)
at sun.security.ssl.X509TrustManagerImpl.checkServerTrusted(X509TrustManagerImpl.java:271)
at sun.security.ssl.ClientHandshaker.serverCertificate(ClientHandshaker.java:1318)
... 11 more

于是进一步,连同HttpClient依赖的版本也使用了低版本4.2,请求该HTTPS链接后,报错信息终于与接入商的报错信息相同了。

1
2
3
4
5
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
<version>4.2</version>
</dependency>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import org.apache.http.HttpResponse;
import org.apache.http.client.HttpClient;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.impl.client.DefaultHttpClient;

public class Application {

public static void main(String[] args) throws Exception {
HttpClient httpClient = new DefaultHttpClient();
HttpGet httpGet = new HttpGet("https://img.wakzz.cn/202004/20200407152546.jpg");
HttpResponse response = httpClient.execute(httpGet);
System.out.println(response.getEntity().getContentLength());
}

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
Exception in thread "main" javax.net.ssl.SSLException: hostname in certificate didn't match: <img.wakzz.cn> != <img.ucdl.pp.uc.cn> OR <img.ucdl.pp.uc.cn> OR <iscsi.ucdl.pp.uc.cn> OR <slient.ucdl.pp.uc.cn> OR <alissl.ucdl.pp.uc.cn> OR <cdn.osupdateservice.yunos.com> OR <oss.ucdl.pp.uc.cn>
at org.apache.http.conn.ssl.AbstractVerifier.verify(AbstractVerifier.java:228)
at org.apache.http.conn.ssl.BrowserCompatHostnameVerifier.verify(BrowserCompatHostnameVerifier.java:54)
at org.apache.http.conn.ssl.AbstractVerifier.verify(AbstractVerifier.java:149)
at org.apache.http.conn.ssl.AbstractVerifier.verify(AbstractVerifier.java:130)
at org.apache.http.conn.ssl.SSLSocketFactory.connectSocket(SSLSocketFactory.java:572)
at org.apache.http.impl.conn.DefaultClientConnectionOperator.openConnection(DefaultClientConnectionOperator.java:180)
at org.apache.http.impl.conn.ManagedClientConnectionImpl.open(ManagedClientConnectionImpl.java:294)
at org.apache.http.impl.client.DefaultRequestDirector.tryConnect(DefaultRequestDirector.java:641)
at org.apache.http.impl.client.DefaultRequestDirector.execute(DefaultRequestDirector.java:480)
at org.apache.http.impl.client.AbstractHttpClient.execute(AbstractHttpClient.java:906)
at org.apache.http.impl.client.AbstractHttpClient.execute(AbstractHttpClient.java:805)
at org.apache.http.impl.client.AbstractHttpClient.execute(AbstractHttpClient.java:784)
at Application.main(Application.java:18)

原理

SNI扩展

一个主机可以绑定多个Web服务,比如某个主机的Nginx配置了a.comb.comc.com等多个域名的代理。当HTTP请求到达该主机的Nginx时,Nginx可以直接解析Http报文中的Host值从而将请求转发给特定的Web服务主机。

但是当HTTPS请求到达该主机的Nginx时就出问题了,每个域名绑定一个证书,当HTTPS握手请求到Nginx时,此时HTTPS还没建立完成,请求报文中并没有Host值,Nginx也就无从得知该给客户端响应哪个域名的证书。

于是在RFC 6066定义了TLS/SSL的SNI扩展server_name,类似于HTTP请求中的Host,客户端请求HTTPS握手时的Client Hello消息中增加一个server_name字段,值为请求主机的域名。当服务端收到该请求时,解析出server_name的值从而给客户端响应对应的域名证书。

JDK支持

Enhancements in Java SE 7中显示,SNI扩展是JDK1.7版本才开始支持,因此在上面复现过程中使用JDK1.6时,HTTPS握手失败。

通过抓包HTTPS握手过程,jdk1.8在发送Client Hello请求时带上了server_name扩展;

image

而在jdk1.6在发送Client Hello请求时并没有server_name扩展;

image

而即使使用了高版本的JDK,当使用低版本的HttpClient请求时,依然会Https握手失败,因为HttpClient是在4.3.2版本才开始支持SNI扩展功能,见Server Name Indication (SNI) Support

通过抓包httpclient4.2的Client Hello请求,显示报文中并没有server_name扩展,因此当请求多域名证书的域名时,会Https握手失败。

image

解决方案

  1. 该服务器主机下只配置一个HTTPS的域名证书;
  2. 客户端JDK版本升级到1.7版本或更高版本;
  3. 客户端HttpClient版本升级到4.3.2版本或更高版本;
>